from __future__ import annotations

import json
import frappe
from frappe.utils import getdate, now_datetime
from typing import Optional, Dict, Any, List

from ..utils.WDT import WMSWDTClient, WDTAPIException
from ..utils.shopify_client import ShopifyClient, ShopifyClientError
from .api import WDT_OWNER_CODE, WDT_WAREHOUSE_CODE


@frappe.whitelist()
def sync_wdt_stock_total(owner_no: str, warehouse_no: str, is_position: int = 1, defect: Optional[int] = None, page_size: int = 100) -> Dict[str, Any]:
    """拉取旺店通定制接口 stock.total.query 的结果，并将当天快照覆盖写入
    (WDT Stock Total Snapshot Inst)。

    覆盖策略：先删除 snapshot_date=今天、owner_no、warehouse_no 匹配的所有记录，再批量插入。
    """
    if not owner_no or not warehouse_no:
        return {"success": False, "error": "owner_no and warehouse_no are required"}

    client = WMSWDTClient()
    today = getdate(now_datetime())

    total_inserted = 0
    page_no = 0
    # 在调用外部接口前，预加载库位体积映射
    position_volume_map = build_position_volume_map(warehouse_no)
    # 先请求第一页，确认有数据再删除，避免异常清空
    resp = client.stock_total_query(owner_no=owner_no, warehouse_no=warehouse_no, is_position=int(is_position or 0), defect=defect, page_size=page_size, page_no=page_no)
    content = (resp or {}).get("content") or []
    if not content:
        return {"success": True, "inserted": 0, "message": "no data returned; skipped cleanup"}

    # spec_no -> company 映射（按当前页批量查询）
    first_specs = list({(it.get("spec_no") or "").strip() for it in content if (it.get("spec_no") or "").strip()})
    spec2comp = _map_spec_to_company(first_specs)

    # 确认有数据后再清理今天的快照（按 is_position/defect 且按公司维度，仅删除未计费的）
    companies = set(spec2comp.values()) if spec2comp else {None}
    for comp in companies:
        if defect is None:
            if comp:
                frappe.db.sql(
                    """
                    delete from `tabWDT Stock Total Snapshot Inst`
                    where snapshot_date=%s and owner_no=%s and warehouse_no=%s and is_position=%s and ifnull(billing_included,0)=0 and company=%s
                    """,
                    (today, owner_no, warehouse_no, 1 if int(is_position or 0) == 1 else 0, comp),
                )
            else:
                frappe.db.sql(
                    """
                    delete from `tabWDT Stock Total Snapshot Inst`
                    where snapshot_date=%s and owner_no=%s and warehouse_no=%s and is_position=%s and ifnull(billing_included,0)=0 and company is null
                    """,
                    (today, owner_no, warehouse_no, 1 if int(is_position or 0) == 1 else 0),
                )
        else:
            if comp:
                frappe.db.sql(
                    """
                    delete from `tabWDT Stock Total Snapshot Inst`
                    where snapshot_date=%s and owner_no=%s and warehouse_no=%s and is_position=%s and defect=%s and ifnull(billing_included,0)=0 and company=%s
                    """,
                    (today, owner_no, warehouse_no, 1 if int(is_position or 0) == 1 else 0, defect, comp),
                )
            else:
                frappe.db.sql(
                    """
                    delete from `tabWDT Stock Total Snapshot Inst`
                    where snapshot_date=%s and owner_no=%s and warehouse_no=%s and is_position=%s and defect=%s and ifnull(billing_included,0)=0 and company is null
                    """,
                    (today, owner_no, warehouse_no, 1 if int(is_position or 0) == 1 else 0, defect),
                )

    # 处理第一页
    for item in content:
        comp = spec2comp.get((item.get("spec_no") or "").strip())
        # 若要求返回库位，校验库位体积映射存在
        if int(is_position or 0) == 1:
            pos_no = item.get("position_no")
            zt = item.get("zone_type")
            if pos_no and zt is not None:
                key = position_key(warehouse_no, str(zt), pos_no)
                if key not in position_volume_map:
                    frappe.logger().error(f"[WDT] position volume missing: {key}")
                    frappe.throw(f"Position volume not configured for warehouse/zone/position={key}")
                _ = position_volume_map.get(key)  # 预留：体积值
        doc = frappe.get_doc({
            "doctype": "WDT Stock Total Snapshot Inst",
            "snapshot_date": today,
            "owner_no": owner_no,
            "warehouse_no": warehouse_no,
            "company": comp,
            "is_position": 1 if int(is_position or 0) == 1 else 0,
            "defect": item.get("defect"),
            "spec_no": item.get("spec_no"),
            "spec_name": item.get("spec_name"),
            "goods_name": item.get("goods_name"),
            "quantity": item.get("quantity"),
            "sending_num": item.get("sending_num"),
            "order_num": item.get("order_num"),
            "available_num": item.get("available_num"),
            "batch_no": item.get("batch_no"),
            "product_date": item.get("product_date"),
            "expire_date": item.get("expire_date"),
            "position_no": item.get("position_no"),
            "zone_type": item.get("zone_type"),
            "raw_payload": json.dumps(item, ensure_ascii=False),
        })
        doc.insert(ignore_permissions=True)
        total_inserted += 1

    # 翻页继续
    while True:
        if len(content) < page_size:
            break
        page_no += 1
        resp = client.stock_total_query(owner_no=owner_no, warehouse_no=warehouse_no, is_position=int(is_position or 0), defect=defect, page_size=page_size, page_no=page_no)
        content = (resp or {}).get("content") or []
        if not content:
            break
        # 每页重新构建映射（避免单页新出现的SKU未映射）
        specs = list({(it.get("spec_no") or "").strip() for it in content if (it.get("spec_no") or "").strip()})
        spec2comp = _map_spec_to_company(specs)
        for item in content:
            comp = spec2comp.get((item.get("spec_no") or "").strip())
            if int(is_position or 0) == 1:
                pos_no = item.get("position_no")
                zt = item.get("zone_type")
                if pos_no and zt is not None:
                    key = position_key(warehouse_no, str(zt), pos_no)
                    if key not in position_volume_map:
                        frappe.logger().error(f"[WDT] position volume missing: {key}")
                        frappe.throw(f"Position volume not configured for warehouse/zone/position={key}")
                    _ = position_volume_map.get(key)
            doc = frappe.get_doc({
                "doctype": "WDT Stock Total Snapshot Inst",
                "snapshot_date": today,
                "owner_no": owner_no,
                "warehouse_no": warehouse_no,
                "company": comp,
                "is_position": 1 if int(is_position or 0) == 1 else 0,
                "defect": item.get("defect"),
                "spec_no": item.get("spec_no"),
                "spec_name": item.get("spec_name"),
                "goods_name": item.get("goods_name"),
                "quantity": item.get("quantity"),
                "sending_num": item.get("sending_num"),
                "order_num": item.get("order_num"),
                "available_num": item.get("available_num"),
                "batch_no": item.get("batch_no"),
                "product_date": item.get("product_date"),
                "expire_date": item.get("expire_date"),
                "position_no": item.get("position_no"),
                "zone_type": item.get("zone_type"),
                "raw_payload": json.dumps(item, ensure_ascii=False),
            })
            doc.insert(ignore_permissions=True)
            total_inserted += 1

    return {"success": True, "inserted": total_inserted}


def _map_spec_to_company(spec_codes: list[str]) -> Dict[str, Optional[str]]:
    """根据 Item Default 反查 SKU 对应公司。返回 dict: spec_no -> company。
    若未配置，返回值中该 SKU 映射为 None。
    """
    if not spec_codes:
        return {}
    rows = frappe.get_all(
        "Item Default",
        filters={
            "parent": ["in", spec_codes],
        },
        fields=["parent", "company"],
        limit_page_length=0,
    )
    m: Dict[str, Optional[str]] = {}
    for r in rows:
        key = r.get("parent")
        if key and key not in m and r.get("company"):
            m[key] = r.get("company")
    # 未找到的显式设为 None 以便上层使用
    for s in spec_codes:
        if s not in m:
            m[s] = None
    return m


@frappe.whitelist()
def get_position_cubic_m(position_no: str) -> float:
    """预留：根据库位编号计算库位体积（立方米）。
    未来将基于单独配置表（如 Position Config）维护库位长宽高并计算，当前返回 0。
    """
    try:
        # TODO: 查配置表计算：length*width*height / 1e6
        return 0.0
    except Exception:
        return 0.0


def build_position_volume_map(warehouse_no: str) -> Dict[str, float]:
    """预加载库位体积映射：position_no -> cubic_m。
    这里对接配置表 `WDT Position Config`，以 (warehouse_no, zone_type, position_no) 为 key。
    """
    try:
        rows = frappe.get_all(
            "WDT Position Config",
            filters={"warehouse_no": warehouse_no},
            fields=["warehouse_no", "zone_type", "position_no", "length_mm", "width_mm", "height_mm", "cubic_m"],
            limit_page_length=0,
        )
        m: Dict[str, float] = {}
        for r in rows:
            key = position_key(r.get("warehouse_no"), str(r.get("zone_type")), r.get("position_no"))
            cube = r.get("cubic_m")
            if cube in (None, ""):
                # 若未填体积，尝试由长宽高(mm)计算
                L = float(r.get("length_mm") or 0)
                W = float(r.get("width_mm") or 0)
                H = float(r.get("height_mm") or 0)
                cube = (L * W * H) / 1_000_000_000.0 if L and W and H else 0.0
            m[key] = float(cube or 0.0)
        return m
    except Exception:
        return {}


def position_key(warehouse_no: str, zone_type: str, position_no: str) -> str:
    return f"{warehouse_no}|{zone_type}|{position_no}"


@frappe.whitelist()
def scheduled_sync_wdt_stock_total() -> Dict[str, Any]:
    """调度入口：使用代码中配置的唯一 owner/warehouse 组合，拉取当日快照。

    说明：
    - 不再从 site_config 读取参数，直接使用 `api.py` 中的 WDT_OWNER_CODE / WDT_WAREHOUSE_CODE。
    - 若后续需要多仓/多货主，可再扩展为列表。
    """
    owners = [WDT_OWNER_CODE]
    whs = [WDT_WAREHOUSE_CODE]
    total = 0
    for o in owners:
        for w in whs:
            try:
                ret = sync_wdt_stock_total(o, w, is_position=0)
                total += int(ret.get("inserted") or 0)
            except Exception:
                frappe.logger().warning(f"[WDT] stock total sync failed for owner={o} wh={w}: {frappe.get_traceback()}")
    return {"success": True, "inserted": total, "owners": owners, "warehouses": whs}


@frappe.whitelist()
def get_wdt_and_shopify_stock(shopname: str, items) -> Dict[str, Any]:
    """
    批量获取指定 SKU 在旺店通仓库 & Shopify 上的库存。

    参数：
    - shopname: usershop 的 name（前端下拉框选择）
    - items: SKU 列表，可以是：
        * JSON 字符串：[{ "sku": "DBRA22-WHTXL", "sku_name": "xxx" }, ...]
        * 或 Python list，元素为 dict / 字符串

    返回：
    {
      "success": true,
      "items": [
        {
          "sku": "DBRA22-WHTXL",
          "sku_name": "xxx",
          "quantity": "15",
          "lockQuantity": "0",
          "batchCode": "",
          "productDate": "",
          "expireDate": "",
          "shopify_on_hand": 9
        },
        ...
      ]
    }
    """

    try:
        if not shopname:
            return {"success": False, "message": "shopname 不能为空"}

        # 解析 items
        raw_items = items
        if isinstance(items, str):
            try:
                raw_items = json.loads(items) if items.strip() else []
            except Exception:
                return {"success": False, "message": "items 参数格式错误（应为 JSON 列表）"}

        if not isinstance(raw_items, list) or not raw_items:
            return {"success": False, "message": "items 必须是非空列表"}

        normalized_items: List[Dict[str, str]] = []
        sku_list: List[str] = []

        for row in raw_items:
            if isinstance(row, dict):
                sku = (row.get("sku") or row.get("item_code") or "").strip()
                name = (
                    row.get("sku_name")
                    or row.get("item_name")
                    or row.get("name")
                    or sku
                )
            else:
                sku = str(row).strip()
                name = sku

            if not sku:
                continue

            normalized_items.append({"sku": sku, "sku_name": name})
            sku_list.append(sku)

        if not normalized_items:
            return {"success": False, "message": "items 中没有有效的 SKU"}

        # -----------------------------
        # 1) 调用 WDT 批量库存查询
        #    注意：wdt_wms_inventory_query 返回的结构为
        #    {flag, code, message, items: {item: [...]}}
        #    这里按 SKU 聚合我们关心的字段
        # -----------------------------
        wdt_client = WMSWDTClient()

        criteria_list = []
        for sku in sku_list:
            criteria_list.append(
                {
                    "warehouseCode": WDT_WAREHOUSE_CODE,
                    "ownerCode": WDT_OWNER_CODE,
                    "itemCode": sku,
                    "inventoryType": "ZP",
                    "remark": f"商品库存查询: {sku}",
                }
            )

        query_data = {
            "criteriaList": criteria_list,
            "remark": f"批量商品库存查询: {', '.join(sku_list[:5])}",
        }

        frappe.logger().error(
            f"[WDT] 批量库存查询参数: {json.dumps(query_data, ensure_ascii=False)}"
        )
        wdt_result = wdt_client.wdt_wms_inventory_query(query_data)
        frappe.logger().error(f"[WDT] 批量库存查询响应: {wdt_result}")

        if not wdt_result:
            return {
                "success": False,
                "message": "WDT 库存查询失败：返回为空",
                "wdt_result": wdt_result,
            }

        # 按旺店通规范：code 为空或 "0" 视为成功
        code = str(wdt_result.get("code", ""))
        if code not in ("", "0"):
            return {
                "success": False,
                "message": f"WDT 库存查询失败: {wdt_result.get('message') or 'unknown'}",
                "wdt_result": wdt_result,
            }

        # 解析 items -> item 列表
        wdt_map: Dict[str, Dict[str, Optional[str]]] = {}
        items_block = wdt_result.get("items") or {}

        inv_rows: List[Dict[str, Any]] = []
        if isinstance(items_block, dict) and "item" in items_block:
            inv_rows = items_block["item"]
            if not isinstance(inv_rows, list):
                inv_rows = [inv_rows]
        elif isinstance(items_block, list):
            inv_rows = items_block

        for inv in inv_rows or []:
            sku = (inv.get("itemCode") or inv.get("item_code") or "").strip()
            if not sku or sku not in sku_list:
                continue
            # 若同一 SKU 有多行，只取第一行（与之前单条查询行为一致）
            if sku in wdt_map:
                continue

            wdt_map[sku] = {
                "quantity": str(inv.get("quantity", "")),
                "lockQuantity": str(
                    inv.get("lockQuantity", inv.get("lock_quantity", ""))
                ),
                "batchCode": inv.get("batchCode", inv.get("batch_no", "")) or "",
                "productDate": inv.get("productDate", inv.get("product_date", "")) or "",
                "expireDate": inv.get("expireDate", inv.get("expire_date", "")) or "",
            }

        # -----------------------------
        # 2) 调用 Shopify 库存查询
        # -----------------------------
        shopify_map: Dict[str, Dict] = {}
        shopify_error: Optional[str] = None

        # 读取 usershop.allowed_locations（按名称，逗号分隔）
        allowed_locations: List[str] = []
        try:
            us = frappe.get_value(
                "usershop",
                {"name": shopname},
                ["allowed_locations"],
                as_dict=True,
            )
            if us and us.get("allowed_locations"):
                raw = us.get("allowed_locations") or ""
                allowed_locations = [
                    part.strip()
                    for part in str(raw).split(",")
                    if part and part.strip()
                ]
        except Exception:
            frappe.logger().warning(
                f"[Shopify] 读取 usershop.allowed_locations 失败 shopname={shopname}: {frappe.get_traceback()}"
            )

        try:
            shop_client = ShopifyClient(shop_name=shopname)
            shopify_map = shop_client.fetch_inventory_by_skus(
                sku_list,
                location_ids=allowed_locations or None,
            )
        except ShopifyClientError as e:
            shopify_error = str(e)
            frappe.logger().error(
                f"[Shopify] 库存查询失败 shopname={shopname}: {shopify_error}"
            )
        except Exception:
            shopify_error = "Shopify 库存查询异常"
            frappe.logger().error(
                f"[Shopify] 库存查询异常 shopname={shopname}: {frappe.get_traceback()}"
            )

        # -----------------------------
        # 3) 组装最终结果 & 写入/更新快照 DocType
        # -----------------------------
        merged_items: List[Dict[str, Any]] = []
        for it in normalized_items:
            sku = it["sku"]
            name = it["sku_name"]

            wdt_info = wdt_map.get(sku, {}) or {}
            shop_info = shopify_map.get(sku) if shopify_map else None

            shopify_on_hand = 0
            shopify_location_name = ""
            shopify_location_id = ""
            shopify_inventory_item_id = ""

            if shop_info:
                try:
                    shopify_on_hand = int(shop_info.get("total_on_hand") or 0)
                except Exception:
                    shopify_on_hand = 0

                # 取第一个变体 / 位置的 inventory_level_id & location_name 作为代表
                try:
                    variants = shop_info.get("variants") or []
                    if variants:
                        first_var = variants[0] or {}
                        shopify_inventory_item_id = first_var.get("inventory_item_id") or ""
                        locs = first_var.get("locations") or []
                        if locs:
                            first_loc = locs[0] or {}
                            shopify_location_name = first_loc.get("location_name") or ""
                            shopify_location_id = first_loc.get("location_id") or ""
                except Exception:
                    pass

            merged_row = {
                "sku": sku,
                "sku_name": name,
                "quantity": wdt_info.get("quantity", ""),
                "lockQuantity": wdt_info.get("lockQuantity", ""),
                "batchCode": wdt_info.get("batchCode", ""),
                "productDate": wdt_info.get("productDate", ""),
                "expireDate": wdt_info.get("expireDate", ""),
                "shopify_on_hand": shopify_on_hand,
                "shopify_location_name": shopify_location_name,
                "shopify_location_id": shopify_location_id,
                "shopify_inventory_item_id": shopify_inventory_item_id,
            }
            merged_items.append(merged_row)

            # 写入 / 更新快照 DocType（每次尽量更新）
            try:
                snapshot_date = getdate(now_datetime())
                existing_names = frappe.get_all(
                    "WDT Shopify Inventory Snapshot Inst",
                    filters={
                        "shopname": shopname,
                        "item": sku,
                    },
                    pluck="name",
                    limit=1,
                )
                if existing_names:
                    doc = frappe.get_doc(
                        "WDT Shopify Inventory Snapshot Inst", existing_names[0]
                    )
                    doc.snapshot_date = snapshot_date
                    doc.last_sync_at = now_datetime()
                else:
                    doc = frappe.get_doc(
                        {
                            "doctype": "WDT Shopify Inventory Snapshot Inst",
                            "snapshot_date": snapshot_date,
                            "shopname": shopname,
                            "item": sku,
                        }
                    )

                doc.item_name = name
                doc.owner_no = WDT_OWNER_CODE
                doc.warehouse_no = WDT_WAREHOUSE_CODE

                # WDT
                try:
                    doc.wdt_quantity = float(wdt_info.get("quantity") or 0)
                except Exception:
                    doc.wdt_quantity = 0
                try:
                    doc.wdt_lock_quantity = float(wdt_info.get("lockQuantity") or 0)
                except Exception:
                    doc.wdt_lock_quantity = 0
                doc.wdt_batch_code = wdt_info.get("batchCode", "")
                doc.wdt_product_date = wdt_info.get("productDate", "")
                doc.wdt_expire_date = wdt_info.get("expireDate", "")

                # Shopify
                doc.shopify_on_hand = float(shopify_on_hand or 0)
                doc.shopify_location_name = shopify_location_name
                doc.shopify_location_id = shopify_location_id
                doc.shopify_inventory_item_id = shopify_inventory_item_id

                doc.save(ignore_permissions=True)
            except Exception:
                frappe.logger().error(
                    f"[WDT+Shopify] 保存快照失败 sku={sku} shopname={shopname}: {frappe.get_traceback()}"
                )

        resp: Dict[str, Any] = {
            "success": True,
            "message": "查询成功",
            "shopname": shopname,
            "items": merged_items,
        }
        if shopify_error:
            resp["shopify_error"] = shopify_error

        return resp

    except Exception:
        # 捕获整个 get_wdt_and_shopify_stock 过程中未预料的异常，避免前端直接报 500
        frappe.logger().error(
            f"[WDT+Shopify] get_wdt_and_shopify_stock 异常 shopname={shopname}: {frappe.get_traceback()}"
        )
        return {
            "success": False,
            "message": "获取 WDT + Shopify 库存快照时发生异常，请查看服务器日志。",
            "error": frappe.get_traceback(),
        }


@frappe.whitelist()
def update_shopify_inventory(
    shopname: str,
    inventory_item_id: str,
    location_id: str,
    on_hand_qty: float,
    reason: Optional[str] = None,
) -> Dict[str, Any]:
    """
    使用 Shopify Admin GraphQL 的 inventorySetQuantities 修改 On hand 库存。

    参数：
    - shopname: usershop.name（Shopify 店铺）
    - inventory_item_id: InventoryItem 的 GID（gid://shopify/InventoryItem/...）
    - location_id: Location 的 GID（gid://shopify/Location/...）
    - on_hand_qty: 目标 on_hand 数量（绝对值）
    - reason: 可选调整原因（写入 Shopify 审计日志）
    """

    if not shopname or not inventory_item_id or not location_id:
        frappe.logger().error(
            f"[Shopify] update_shopify_inventory 参数缺失 "
            f"shopname={shopname}, inventory_item_id={inventory_item_id}, location_id={location_id}"
        )
        return {
            "success": False,
            "message": "shopname、inventory_item_id、location_id 均不能为空",
        }

    try:
        client = ShopifyClient(shop_name=shopname)

        mutation = """
        mutation SetQuantities($input: InventorySetQuantitiesInput!) {
          inventorySetQuantities(input: $input) {
            inventoryAdjustmentGroup {
              reason
              changes {
                name
                delta
                quantityAfterChange
              }
            }
            userErrors {
              code
              field
              message
            }
          }
        }
        """

        # Shopify GraphQL 这里的 quantity 类型是 Int，不能传 50.0 这样的浮点数
        # reason 统一固定使用 "correction"
        input_obj: Dict[str, Any] = {
            "name": "on_hand",
            "ignoreCompareQuantity": True,
            "quantities": [
                {
                    "inventoryItemId": inventory_item_id,
                    "locationId": location_id,
                    "quantity": int(on_hand_qty),
                }
            ],
            "reason": "correction",
        }

        variables = {"input": input_obj}

        frappe.logger().error(
            f"[Shopify] update_shopify_inventory 请求 "
            f"shopname={shopname}, inventory_item_id={inventory_item_id}, "
            f"location_id={location_id}, on_hand_qty={on_hand_qty}, variables={variables}"
        )

        data = client._post_graphql(mutation, variables)  # type: ignore[attr-defined]
        payload = data.get("inventorySetQuantities") or {}
        user_errors = payload.get("userErrors") or []

        if user_errors:
            frappe.logger().error(
                "[Shopify] update_shopify_inventory GraphQL userErrors: "
                f"shopname={shopname}, inventory_item_id={inventory_item_id}, "
                f"location_id={location_id}, on_hand_qty={on_hand_qty}, "
                f"user_errors={user_errors}, payload={payload}"
            )
            return {
                "success": False,
                "message": "; ".join(
                    err.get("message") for err in user_errors if err.get("message")
                ),
                "user_errors": user_errors,
                "data": payload,
            }

        group = payload.get("inventoryAdjustmentGroup") or {}
        changes = group.get("changes") or []
        new_on_hand = None
        if changes:
            try:
                # 如果 Shopify 返回 quantityAfterChange 则优先使用
                for ch in changes:
                    if ch.get("name") == "on_hand":
                        new_on_hand = ch.get("quantityAfterChange")
                        break
            except Exception:
                new_on_hand = None

        return {
            "success": True,
            "message": "Shopify 库存已更新",
            "data": {
                "inventoryAdjustmentGroup": group,
                "new_on_hand": new_on_hand,
            },
        }

    except ShopifyClientError as e:
        frappe.logger().error(
            f"[Shopify] update_shopify_inventory 失败 shopname={shopname}: {str(e)}"
        )
        return {"success": False, "message": str(e)}
    except Exception as e:
        frappe.logger().error(
            f"[Shopify] update_shopify_inventory 异常 shopname={shopname}: {frappe.get_traceback()}"
        )
        return {"success": False, "message": str(e)}


@frappe.whitelist()
def init_inventory_snapshot(shopname: str) -> Dict[str, Any]:
    """
    初始化 WDT Shopify Inventory Snapshot Inst：
    - 以当前用户可访问的所有 Item 作为基准
    - 为指定 shopname 创建或更新快照记录（仅基础字段，不拉取库存）
    """
    if not shopname:
        return {"success": False, "message": "shopname 不能为空"}

    items = frappe.get_all(
        "Item",
        fields=["name", "item_name"],
        limit_page_length=0,
    )
    if not items:
        return {"success": False, "message": "当前用户无可用 Item"}

    snapshot_date = getdate(now_datetime())
    created = 0
    updated = 0

    for it in items:
        sku = it.get("name")
        if not sku:
            continue
        item_name = it.get("item_name") or sku

        existing_names = frappe.get_all(
            "WDT Shopify Inventory Snapshot Inst",
            filters={
                "shopname": shopname,
                "item": sku,
            },
            pluck="name",
            limit=1,
        )
        if existing_names:
            doc = frappe.get_doc(
                "WDT Shopify Inventory Snapshot Inst", existing_names[0]
            )
            doc.snapshot_date = snapshot_date
            doc.last_sync_at = now_datetime()
            updated += 1
        else:
            doc = frappe.get_doc(
                {
                    "doctype": "WDT Shopify Inventory Snapshot Inst",
                    "snapshot_date": snapshot_date,
                    "shopname": shopname,
                    "item": sku,
                }
            )
            created += 1

        doc.item_name = item_name
        # 默认关联当前固定的 WDT owner/warehouse，后续可扩展
        doc.owner_no = WDT_OWNER_CODE
        doc.warehouse_no = WDT_WAREHOUSE_CODE

        doc.save(ignore_permissions=True)

    return {
        "success": True,
        "message": "初始化完成",
        "created": created,
        "updated": updated,
    }


@frappe.whitelist()
def refresh_wdt_stock_snapshot(shopname: str) -> Dict[str, Any]:
    """
    根据快照中已有的 Item，批量从旺店通刷新仓库库存字段。
    """
    if not shopname:
        return {"success": False, "message": "shopname 不能为空"}

    rows = frappe.get_all(
        "WDT Shopify Inventory Snapshot Inst",
        filters={"shopname": shopname},
        fields=["name", "item"],
        limit_page_length=0,
    )
    if not rows:
        return {"success": False, "message": "当前 shopname 下没有任何快照记录"}

    sku_list = [r["item"] for r in rows if r.get("item")]
    sku_list = list({s for s in sku_list if s})  # 去重
    if not sku_list:
        return {"success": False, "message": "快照记录中没有有效的 Item 编码"}

    # 调用 WDT 获取库存
    wdt_client = WMSWDTClient()

    criteria_list = []
    for sku in sku_list:
        criteria_list.append(
            {
                "warehouseCode": WDT_WAREHOUSE_CODE,
                "ownerCode": WDT_OWNER_CODE,
                "itemCode": sku,
                "inventoryType": "ZP",
                "remark": f"商品库存查询: {sku}",
            }
        )

    query_data = {
        "criteriaList": criteria_list,
        "remark": f"批量商品库存查询(刷新快照): {', '.join(sku_list[:5])}",
    }

    wdt_result = wdt_client.wdt_wms_inventory_query(query_data)
    if not wdt_result:
        return {
            "success": False,
            "message": "WDT 库存查询失败：返回为空",
            "wdt_result": wdt_result,
        }

    # 按旺店通规范：code 为空或 "0" 视为成功
    code = str(wdt_result.get("code", ""))
    if code not in ("", "0"):
        return {
            "success": False,
            "message": f"WDT 库存查询失败: {wdt_result.get('message') or 'unknown'}",
            "wdt_result": wdt_result,
        }

    # 解析 WDT 返回：items -> item 列表，按 SKU 聚合
    wdt_map: Dict[str, Dict[str, Optional[str]]] = {}
    items_block = wdt_result.get("items") or {}

    inv_rows: List[Dict[str, Any]] = []
    if isinstance(items_block, dict) and "item" in items_block:
        inv_rows = items_block["item"]
        if not isinstance(inv_rows, list):
            inv_rows = [inv_rows]
    elif isinstance(items_block, list):
        inv_rows = items_block

    for inv in inv_rows or []:
        sku = (inv.get("itemCode") or inv.get("item_code") or "").strip()
        if not sku or sku not in sku_list:
            continue
        if sku in wdt_map:
            continue

        wdt_map[sku] = {
            "quantity": str(inv.get("quantity", "")),
            "lockQuantity": str(
                inv.get("lockQuantity", inv.get("lock_quantity", ""))
            ),
            "batchCode": inv.get("batchCode", inv.get("batch_no", "")) or "",
            "productDate": inv.get("productDate", inv.get("product_date", "")) or "",
            "expireDate": inv.get("expireDate", inv.get("expire_date", "")) or "",
        }

    updated = 0
    missed = 0
    snapshot_date = getdate(now_datetime())

    for r in rows:
        sku = r.get("item")
        name = r.get("name")
        info = wdt_map.get(sku)
        if not info:
            missed += 1
            continue

        try:
            doc = frappe.get_doc("WDT Shopify Inventory Snapshot Inst", name)
            doc.snapshot_date = snapshot_date
            doc.last_sync_at = now_datetime()

            try:
                doc.wdt_quantity = float(info.get("quantity") or 0)
            except Exception:
                doc.wdt_quantity = 0
            try:
                doc.wdt_lock_quantity = float(info.get("lockQuantity") or 0)
            except Exception:
                doc.wdt_lock_quantity = 0
            doc.wdt_batch_code = info.get("batchCode", "")
            doc.wdt_product_date = info.get("productDate", "")
            doc.wdt_expire_date = info.get("expireDate", "")

            doc.save(ignore_permissions=True)
            updated += 1
        except Exception:
            frappe.logger().error(
                f"[WDT] 刷新快照 WDT 库存失败 sku={sku} name={name}: {frappe.get_traceback()}"
            )

    return {
        "success": True,
        "message": "WDT 库存刷新完成",
        "updated": updated,
        "missed": missed,
    }


@frappe.whitelist()
def refresh_shopify_stock_snapshot(shopname: str) -> Dict[str, Any]:
    """
    根据快照中已有的 Item，批量从 Shopify 刷新库存字段。
    """
    if not shopname:
        return {"success": False, "message": "shopname 不能为空"}

    rows = frappe.get_all(
        "WDT Shopify Inventory Snapshot Inst",
        filters={"shopname": shopname},
        fields=["name", "item", "item_name"],
        limit_page_length=0,
    )
    if not rows:
        return {"success": False, "message": "当前 shopname 下没有任何快照记录"}

    sku_list = [r["item"] for r in rows if r.get("item")]
    sku_list = list({s for s in sku_list if s})
    if not sku_list:
        return {"success": False, "message": "快照记录中没有有效的 Item 编码"}

    # 调用 Shopify 客户端
    shop_client = ShopifyClient(shop_name=shopname)

    # 读取 usershop.allowed_locations（按名称，逗号分隔）
    allowed_locations: List[str] = []
    try:
        us = frappe.get_value(
            "usershop",
            {"name": shopname},
            ["allowed_locations"],
            as_dict=True,
        )
        if us and us.get("allowed_locations"):
            raw = us.get("allowed_locations") or ""
            allowed_locations = [
                part.strip()
                for part in str(raw).split(",")
                if part and part.strip()
            ]
    except Exception:
        frappe.logger().warning(
            f"[Shopify] 读取 usershop.allowed_locations 失败 shopname={shopname}: {frappe.get_traceback()}"
        )

    shopify_map = shop_client.fetch_inventory_by_skus(
        sku_list,
        location_ids=allowed_locations or None,
    )

    updated = 0
    missed = 0
    snapshot_date = getdate(now_datetime())

    for r in rows:
        sku = r.get("item")
        name = r.get("name")
        shop_info = shopify_map.get(sku)
        if not shop_info:
            missed += 1
            continue

        shopify_on_hand = 0
        shopify_location_name = ""
        shopify_location_id = ""
        shopify_inventory_item_id = ""

        try:
            shopify_on_hand = int(shop_info.get("total_on_hand") or 0)
        except Exception:
            shopify_on_hand = 0

        try:
            variants = shop_info.get("variants") or []
            if variants:
                first_var = variants[0] or {}
                shopify_inventory_item_id = first_var.get("inventory_item_id") or ""
                locs = first_var.get("locations") or []
                if locs:
                    first_loc = locs[0] or {}
                    shopify_location_name = first_loc.get("location_name") or ""
                    shopify_location_id = first_loc.get("location_id") or ""
        except Exception:
            pass

        try:
            doc = frappe.get_doc("WDT Shopify Inventory Snapshot Inst", name)
            doc.snapshot_date = snapshot_date
            doc.last_sync_at = now_datetime()

            doc.shopify_on_hand = float(shopify_on_hand or 0)
            doc.shopify_location_name = shopify_location_name
            doc.shopify_location_id = shopify_location_id
            doc.shopify_inventory_item_id = shopify_inventory_item_id

            doc.save(ignore_permissions=True)
            updated += 1
        except Exception:
            frappe.logger().error(
                f"[Shopify] 刷新快照 Shopify 库存失败 sku={sku} name={name}: {frappe.get_traceback()}"
            )

    return {
        "success": True,
        "message": "Shopify 库存刷新完成",
        "updated": updated,
        "missed": missed,
    }


@frappe.whitelist()
def bulk_update_shopify_inventory(docnames) -> Dict[str, Any]:
    """
    批量根据快照记录更新 Shopify 库存。

    参数：
    - docnames: 列表或 JSON 字符串，元素为快照 DocType 的 name。
    """
    if isinstance(docnames, str):
        try:
            docnames = json.loads(docnames) if docnames.strip() else []
        except Exception:
            return {"success": False, "message": "docnames 参数格式错误"}

    if not isinstance(docnames, list) or not docnames:
        return {"success": False, "message": "docnames 必须是非空列表"}

    updated = 0
    failed = 0
    errors: List[Dict[str, Any]] = []

    for name in docnames:
        try:
            doc = frappe.get_doc("WDT Shopify Inventory Snapshot Inst", name)
            if (
                not doc.shopname
                or not doc.shopify_inventory_item_id
                or not doc.shopify_location_id
            ):
                failed += 1
                frappe.logger().error(
                    "[Shopify] bulk_update_shopify_inventory 跳过记录：缺少关键字段 "
                    f"name={name}, shopname={getattr(doc, 'shopname', None)}, "
                    f"inventory_item_id={getattr(doc, 'shopify_inventory_item_id', None)}, "
                    f"location_id={getattr(doc, 'shopify_location_id', None)}"
                )
                errors.append(
                    {
                        "name": name,
                        "message": "缺少 shopname 或 Shopify Inventory Item/Location ID",
                    }
                )
                continue

            # 若 WDT Quantity 小于等于 0，则根据业务规则跳过，不回写 Shopify
            target_qty = int(doc.wdt_quantity or 0)
            if target_qty <= 0:
                failed += 1
                errors.append(
                    {
                        "name": name,
                        "message": "WDT Quantity 小于等于 0，已跳过 Shopify 库存更新",
                    }
                )
                continue

            # 批量回写时，统一以 WDT Quantity 作为 Shopify 的目标 on_hand 库存（使用 Int）
            resp = update_shopify_inventory(
                shopname=doc.shopname,
                inventory_item_id=doc.shopify_inventory_item_id,
                location_id=doc.shopify_location_id,
                on_hand_qty=target_qty,
                reason="bulk_sync_from_snapshot",
            )

            if resp.get("success"):
                updated += 1
            else:
                failed += 1
                frappe.logger().error(
                    "[Shopify] bulk_update_shopify_inventory 单条更新失败 "
                    f"name={name}, shopname={doc.shopname}, "
                    f"inventory_item_id={doc.shopify_inventory_item_id}, "
                    f"location_id={doc.shopify_location_id}, "
                    f"on_hand={doc.shopify_on_hand}, resp={resp}"
                )
                errors.append(
                    {
                        "name": name,
                        "message": resp.get("message"),
                    }
                )
        except Exception:
            failed += 1
            frappe.logger().error(
                "[Shopify] bulk_update_shopify_inventory 单条更新异常 "
                f"name={name}, trace={frappe.get_traceback()}"
            )
            errors.append(
                {
                    "name": name,
                    "message": frappe.get_traceback(),
                }
            )

    return {
        "success": True,
        "message": "批量更新完成",
        "updated": updated,
        "failed": failed,
        "errors": errors,
    }


@frappe.whitelist()
def get_accessible_usershops() -> Dict[str, Any]:
    """
    返回当前用户可访问的 usershop 列表。
    依赖 Frappe 权限系统自动过滤，无需额外限制。
    """
    shops = frappe.get_all(
        "usershop",
        fields=["name"],
        limit_page_length=0,
    )
    return {"success": True, "shops": shops}

